﻿using System;
using System.Collections.Generic;
using System.Linq;
using ClosedXML.Excel;
using MFilesAPI;

namespace FSAG.UserImporter
{
    /// <summary>
    /// Provides methods and properties for mfiles issues regarding user and login accounts
    /// </summary>
    public static class UserManager
    {
        private static Dictionary<LoginAccount, string> AccountPasswordMap { get; set; }
        private static Dictionary<UserAccount, LoginAccount> UserAccountLoginAccountMap { get; set; }
        private static Dictionary<UserAccount, LoginAccount> DeletedUserAccountLoginAccountMap { get; set; }
        private static List<UserAccount> DeletedUserAccounts { get; set; }
        private static Dictionary<UserAccount, bool> UpdatedUserAccounts { get; set; }
        private static List<User> ModifiedUsers { get; set; }
        private static MFilesServerApplication MFilesServerApplication { get; set; }
        private static Vault AcademiesVault { get; set; }
        private static string Path { get; set; }

        /// <summary>
        /// Initialize all necessary variables, sould be called as first method
        /// </summary>
        /// <param name="mFilesServerApplication">Basic MFiles Server Application instance</param>
        /// <param name="vault">Instance to the vault object</param>
        /// <param name="path">Location path to the excel user list</param>
        public static void Initialize(MFilesServerApplication mFilesServerApplication, Vault vault, string path)
        {
            MFilesServerApplication = mFilesServerApplication;
            AcademiesVault = vault;
            Path = path;
            AccountPasswordMap = new Dictionary<LoginAccount, string>();
            UserAccountLoginAccountMap = new Dictionary<UserAccount, LoginAccount>();
            DeletedUserAccountLoginAccountMap = new Dictionary<UserAccount, LoginAccount>();
            DeletedUserAccounts = new List<UserAccount>();
            UpdatedUserAccounts = new Dictionary<UserAccount, bool>();
            ModifiedUsers = new List<User>();
        }

        /// <summary>
        /// Assigns modified users to the defined groups
        /// </summary>
        /// <returns>Returns true if assigning was successful</returns>
        public static bool StartAssignGroups()
        {
            foreach (User modifiedUser in ModifiedUsers)
            {
                if (!UserExistsOnServer(0,modifiedUser))
                {
                    continue;
                }

                foreach (UserGroup userGroup in AcademiesVault.UserGroupOperations.GetUserGroups())
                {
                    if (modifiedUser.AccountID != 0 && userGroup.ID != 1 && userGroup.ID != 2)
                    {
                        if (modifiedUser.UserGroup1 == userGroup.Name || modifiedUser.UserGroup2 == userGroup.Name ||
                            modifiedUser.UserGroup3 == userGroup.Name)
                        {
                            ConfigManager.Log.DebugFormat("Benutzer {0} wird Gruppe {1} hinzugefügt",Helper.DefaultIfNull(modifiedUser.FullName),Helper.DefaultIfNull(userGroup.Name));
                            userGroup.AddMember(modifiedUser.AccountID);
                            AcademiesVault.UserGroupOperations.UpdateUserGroupAdmin(new UserGroupAdminClass {UserGroup = userGroup});
                        }
                        else
                        {
                            ConfigManager.Log.DebugFormat("Benutzer {0} wird aus Gruppe {1} entfernt", Helper.DefaultIfNull(modifiedUser.FullName), Helper.DefaultIfNull(userGroup.Name));
                            userGroup.RemoveMember(modifiedUser.AccountID);
                            AcademiesVault.UserGroupOperations.UpdateUserGroupAdmin(new UserGroupAdminClass {UserGroup = userGroup});
                        }
                        AddUserToUserAccountList(modifiedUser, UpdatedUserAccounts);
                    }
                }
            }
            return true;
        }

        /// <summary>
        /// Used to import respectively update/create the users based on the excel list
        /// </summary>
        /// <param name="mFilesServerApplication">Basic MFiles Server Application instance</param>
        /// <param name="vault">Instance to the vault object</param>
        /// <param name="path">Location path to the excel user list</param>
        /// <returns>Returns true, if the import was successful</returns>
        public static bool StartImportUsers(MFilesServerApplication mFilesServerApplication, Vault vault, string path)
        {
            Initialize(mFilesServerApplication, vault, path);
            if (AcademiesVault == null || vault == null)
                return false;

            if (LoadAccounts())
            {
                CreateUserAccounts();
                CreateLoginAccounts();
                DeleteAllDefinedAccounts();
                return true;
            }
            return false;
        }

        /// <summary>
        /// Used to update the excel list after the import procedure
        /// </summary>
        /// <param name="workbook">Workbook instance for updating values</param>
        public static void FinalizeUserImport(XLWorkbook workbook)
        {
            if (Path != null && workbook != null)
            {
                ConfigManager.Log.Info("Benutzerliste wird aktualisiert..");
                IXLWorksheet worksheet = workbook.Worksheet(1);
                IXLRange rngUserNames = worksheet.Range(ConfigManager.GetSetting("UserNameRange"));
                IXLRange rngUserFullNames = worksheet.Range(ConfigManager.GetSetting("UserFullNameRange"));

                UpdateAccountInformation(rngUserNames, worksheet, UpdatedUserAccounts.Where(pair => pair.Value).Select(pair => pair.Key));
                UpdateAccountInformation(rngUserNames, worksheet, DeletedUserAccounts, true);
                DeleteUnusedUser(rngUserFullNames,workbook.Worksheet(1));
                UpdateStatusInformation(worksheet);

                ConfigManager.Log.Info("Benutzerliste wurde aktualisiert (modified gelöscht, IDs aktualisiert)");
            }
            else
            {
                ConfigManager.Log.Fatal("Pfad oder Workbook objekt in FinalizeUserImport nicht gesetzt");
            }
            
        }

        /// <summary>
        /// This Method is used to remove all old obsolete user entries which contains a not existing id and no user name
        /// </summary>
        /// <param name="rangeFullName">Excel Range for all full names</param>
        /// <param name="worksheet">Worksheet instance of the user list</param>
        private static void DeleteUnusedUser(IXLRangeBase rangeFullName, IXLWorksheet worksheet)
        {
            foreach (IXLCell userFullNameCell in rangeFullName.Cells(cell => cell.WorksheetRow().RowNumber() < ConfigManager.GetSettingAsNumber("LastUsedRow")))
            {
                int userIDNumber;
                string userID =
                    worksheet.Cell(userFullNameCell.WorksheetRow().RowNumber(), ConfigManager.GetSettingAsNumber("ColumnUserID")).Value.ToString().Trim();

                var result = Int32.TryParse(userID, out userIDNumber);
                if (!result || !UserExistsOnServer(userIDNumber))
                {
                    if (string.IsNullOrEmpty(userFullNameCell.Value.ToString().Trim()))
                    {
                        SetDefaultValues(userFullNameCell.WorksheetRow().RowNumber(),worksheet);
                    }
                }
            }
        }

        /// <summary>
        /// Used to set all cell values of a specific row number to the default 
        /// </summary>
        /// <param name="rowNumber">Row number to set default values</param>
        /// <param name="worksheet">Worksheet instance with user list</param>
        private static void SetDefaultValues(int rowNumber, IXLWorksheet worksheet)
        {
            if (rowNumber < ConfigManager.GetSettingAsNumber("FirstUsedRow") || worksheet == null)
            {
                return;
            }
            worksheet.Range(rowNumber, ConfigManager.GetSettingAsNumber("ColumnFirstToClear"), rowNumber, ConfigManager.GetSettingAsNumber("ColumnLastToClear")).Clear(XLClearOptions.Contents);
            worksheet.Range(rowNumber, ConfigManager.GetSettingAsNumber("ColumnFirstToClear2"), rowNumber, ConfigManager.GetSettingAsNumber("ColumnLastToClear2")).Clear(XLClearOptions.Contents);
            worksheet.Cell(rowNumber, ConfigManager.GetSettingAsNumber("ColumnStatus")).Value = ConfigManager.GetSetting("DefaultStatus");
            worksheet.Cell(rowNumber, ConfigManager.GetSettingAsNumber("ColumnLanguage")).Value = ConfigManager.GetSetting("DefaultLanguage");
            worksheet.Range(rowNumber, ConfigManager.GetSettingAsNumber("ColumnFirstPermission"), rowNumber, ConfigManager.GetSettingAsNumber("ColumnLastPermission")).Value = ConfigManager.GetSetting("DefaultPermission");
            worksheet.Cell(rowNumber, ConfigManager.GetSettingAsNumber("ColumnUserID")).Clear(XLClearOptions.Contents);
        }

        /// <summary>
        /// Helper method for adding a defined user to the user account list
        /// </summary>
        /// <param name="user">User to add</param>
        /// <param name="userAccounts">List with all userAccounts</param>
        private static void AddUserToUserAccountList(User user, Dictionary<UserAccount, bool> userAccounts)
        {
            if (userAccounts == null || user == null) return;
            if (userAccounts.Any(userAccountSuccessful => userAccountSuccessful.Key.ID == user.AccountID))
            {
                return;
            }
            userAccounts.Add((UserAccountClass) user, true);
        }

        /// <summary>
        /// Method used to delete all user accounts, which are marked as delete
        /// </summary>
        private static void DeleteAllDefinedAccounts()
        {
            if (DeletedUserAccountLoginAccountMap.Count != 0)
            {
                ConfigManager.Log.Info("Zu löschende Benutzer werden gelöscht");
            }

            foreach (var userLoginAccountPair in DeletedUserAccountLoginAccountMap)
            {
                if (!UserExistsOnServer(userLoginAccountPair.Key.ID))
                {
                    ConfigManager.Log.DebugFormat("Zu löschender Benutzer {0} bereits gelöscht", Helper.DefaultIfNull(userLoginAccountPair.Key.LoginName));
                    DeletedUserAccounts.Add(userLoginAccountPair.Key);
                    continue;
                }

                var userAccount = AcademiesVault.UserOperations.GetUserAccount(userLoginAccountPair.Key.ID);
                if (userAccount != null)
                {
                    AcademiesVault.UserOperations.RemoveUserAccount(userAccount.ID);
                    MFilesServerApplication.LoginAccountOperations.RemoveLoginAccount(userAccount.LoginName);
                    ConfigManager.Log.InfoFormat("* Benutzer {0} wurde gelöscht",Helper.DefaultIfNull(userAccount.LoginName));
                    if (!string.IsNullOrEmpty(userLoginAccountPair.Value.FullName.Trim()))
                    {
                        DeletedUserAccounts.Add(userAccount);
                    }
                }
            }
        }

        /// <summary>
        /// Updates the status information, which could be from type error or info, whereby the status is written to the excel
        /// </summary>
        /// <param name="worksheet">Worksheet instance with the user list</param>
        private static void UpdateStatusInformation(IXLWorksheet worksheet)
        {
            if (worksheet == null) return;
            worksheet.Range(ConfigManager.GetSetting("RangeStatus")).Clear(XLClearOptions.Contents);
            if (!ExcelStatusManager.IsEmpty)
            {
                foreach (IXLCell statusCell in worksheet.Range(ConfigManager.GetSetting("RangeStatusTypes")).Cells())
                {
                    if (ExcelStatusManager.StatusMessageQueue.Count == 0)
                    {
                        return;
                    }

                    ExcelStatus statusMessage = ExcelStatusManager.StatusMessageQueue.Dequeue();
                    worksheet.Cell(statusCell.WorksheetRow().RowNumber(), ConfigManager.GetSettingAsNumber("ColumnDateStatusMessage")).Value =DateTime.Now;
                    statusCell.Value = statusMessage.StatusTypeAsString;
                    worksheet.Cell(statusCell.WorksheetRow().RowNumber(), ConfigManager.GetSettingAsNumber("ColumnStatusMessage")).Value = statusMessage.StatusMessage;
                    ConfigManager.Log.DebugFormat("Status {0} wurde in Excel hinzugefügt",Helper.DefaultIfNull(statusMessage));
                }
            }
        }

        /// <summary>
        /// Combined method for updating some user data entries depending on the parameter
        /// </summary>
        /// <param name="rangeUsernames">Excel range with all usernames</param>
        /// <param name="worksheet">Worksheet instance with user list</param>
        /// <param name="userAccounts">List of user accounts used for updates</param>
        /// <param name="deleted">If set, the id will be reset (set to 0)</param>
        /// <param name="deleteUpdatedTime">If set, the server timestamp, id and username is cleared</param>
        private static void UpdateAccountInformation(IXLRangeBase rangeUsernames, IXLWorksheet worksheet,
                                                     IEnumerable<UserAccount> userAccounts, bool deleted = false,
                                                     bool deleteUpdatedTime = false)
        {
            int userID = 0;
            foreach (UserAccount userAccount in userAccounts)
            {
                foreach (IXLCell userName in rangeUsernames.CellsUsed())
                {
                    if (userAccount.LoginName.Equals(userName.Value))
                    {
                        if (!deleted) userID = userAccount.ID;
                        worksheet.Cell(userName.WorksheetRow().RowNumber(), ConfigManager.GetSettingAsNumber("ColumnUserID")).Value = userID;
                        worksheet.Cell(userName.WorksheetRow().RowNumber(), ConfigManager.GetSettingAsNumber("ColumnModifiedUser"))
                                 .Clear(XLClearOptions.Contents);

                        if (deleteUpdatedTime)
                        {
                            worksheet.Cell(userName.WorksheetRow().RowNumber(), ConfigManager.GetSettingAsNumber("ColumnUserName"))
                                     .Clear(XLClearOptions.Contents);
                            worksheet.Cell(userName.WorksheetRow().RowNumber(),
                                           ConfigManager.GetSettingAsNumber("ColumnUpdatedOnServerUser")).Clear(XLClearOptions.Contents);
                            worksheet.Cell(userName.WorksheetRow().RowNumber(), ConfigManager.GetSettingAsNumber("ColumnUserID"))
                                     .Clear(XLClearOptions.Contents);
                        }
                        else
                        {
                            worksheet.Cell(userName.WorksheetRow().RowNumber(),
                                           ConfigManager.GetSettingAsNumber("ColumnUpdatedOnServerUser"))
                                     .Value = DateTime.Now;
                        }
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Creates or Modifies the mfiles login accounts based on the imported users
        /// </summary>
        public static void CreateLoginAccounts()
        {
            var loginAccountManager = MFilesServerApplication.LoginAccountOperations;
            foreach (var loginAccountPasswordPair in AccountPasswordMap)
            {
                UserAccount relatedUserAccount =
                    UserAccountLoginAccountMap.Keys.ToList()
                                              .Find(account => account.LoginName.Equals(loginAccountPasswordPair.Key.AccountName));

                if (relatedUserAccount == null || GetUserID(relatedUserAccount) == 0)
                {
                    ConfigManager.Log.DebugFormat("Beim Hinzufügen LoginAccount {0} kein passender UserAccount gefunden",Helper.DefaultIfNull(loginAccountPasswordPair.Key.AccountName));
                    return;
                }

                if (LoginAccountExistsOnServer(loginAccountPasswordPair.Key))
                {
                    foreach (LoginAccount loginAccount in loginAccountManager.GetLoginAccounts())
                    {
                        if (loginAccountPasswordPair.Key.UserName == loginAccount.UserName)
                        {
                            try
                            {
                                loginAccountManager.ModifyLoginAccount(loginAccountPasswordPair.Key);
                                ConfigManager.Log.InfoFormat("* Benutzer {0} wurde aktualisiert",Helper.DefaultIfNull(loginAccountPasswordPair.Key.UserName));
                            }
                            catch (Exception)
                            {
                                ConfigManager.Log.Warn("Nicht genügend Lizenzen vorhanden");
                                ExcelStatusManager.AddStatusMessage("Nicht genügend Lizenzen vorhanden");
                                SetStateUpdatedUserAccounts(loginAccount.UserName, false);
                            }
                        }
                    }
                }
                else
                {
                    AddLoginAccount(loginAccountPasswordPair.Key, loginAccountPasswordPair.Value);
                }
            }
        }

        /// <summary>
        /// Method used to set the result of the update of a specific user
        /// </summary>
        /// <param name="username">Username of the user to update</param>
        /// <param name="successful">True, if the update procedure was successful</param>
        private static void SetStateUpdatedUserAccounts(string username, bool successful = true)
        {
            UserAccount userAccount = UpdatedUserAccounts.SingleOrDefault(pair => pair.Key.LoginName == username).Key;
            if (userAccount != null)
            {
                UpdatedUserAccounts[userAccount] = successful;
            }
        }

        /// <summary>
        /// Used for creating a mfiles loginaccount with a specified password
        /// </summary>
        /// <param name="loginAccount">The loginaccount to create on mfiles</param>
        /// <param name="password">The password for the given login account</param>
        private static void AddLoginAccount(LoginAccount loginAccount, string password)
        {
            if (loginAccount == null) return;
            ServerLoginAccountOperations loginAccountManager = MFilesServerApplication.LoginAccountOperations;

            try
            {
                loginAccountManager.AddLoginAccount(loginAccount, password);
                ConfigManager.Log.InfoFormat("* Benutzer {0} wurde erstellt", Helper.DefaultIfNull(loginAccount.UserName));
            }
            catch (Exception)
            {
                ConfigManager.Log.Warn("Nicht genügend Lizenzen vorhanden");
                ExcelStatusManager.AddStatusMessage("Nicht genügend Lizenzen vorhanden");
                foreach (UserAccount user in AcademiesVault.UserOperations.GetUserAccounts())
                {
                    if (user.LoginName == loginAccount.UserName)
                    {
                        AcademiesVault.UserOperations.RemoveUserAccount(user.ID);
                        ConfigManager.Log.Debug("LoginAccount konnte nicht erstellt werden, zugehöriger UserAccount wurde gelöscht");
                        break;
                    }
                }
                SetStateUpdatedUserAccounts(loginAccount.UserName, false);
            }
        }

        /// <summary>
        /// Creates or Modifies the mfiles user accounts based on the imported users
        /// </summary>
        public static void CreateUserAccounts()
        {
            VaultUserOperations userAccountManager = AcademiesVault.UserOperations;
            foreach (UserAccount userAccount in UserAccountLoginAccountMap.Keys)
            {
                userAccount.ID = GetUserID(userAccount);
                UserAccount updatedUserAccount;
                if (userAccount.ID != 0)
                {
                    try
                    {
                        userAccountManager.ModifyUserAccount(userAccount);
                        updatedUserAccount = userAccount;
                    }
                    catch (Exception)
                    {
                        ConfigManager.Log.ErrorFormat("Benutzer {0} konnte nicht aktualisiert werden, da nicht vorhanden", Helper.DefaultIfNull(userAccount.LoginName));
                        ExcelStatusManager.AddStatusMessage(
                            string.Format("{0} konnte nicht aktualisiert werden, da nicht vorhanden!", userAccount.LoginName),
                            StatusType.Exception);
                        continue;
                    }
                }
                else
                {
                    try
                    {
                        updatedUserAccount = userAccountManager.AddUserAccount(userAccount);
                        User newUser = ModifiedUsers.Find(user => user.UserName == updatedUserAccount.LoginName);
                        newUser.AccountID = updatedUserAccount.ID;
                    }
                    catch (Exception)
                    {
                        ConfigManager.Log.ErrorFormat("Benutzer {0} konnte nicht erstellt werden, da bereits vorhanden", Helper.DefaultIfNull(userAccount.LoginName));
                        ExcelStatusManager.AddStatusMessage(
                            string.Format("{0} konnte nicht erstellt werden, da bereits vorhanden!", userAccount.LoginName),
                            StatusType.Exception);
                        continue;
                    }
                }
                if (updatedUserAccount.ID == 0) continue;
                UpdatedUserAccounts.Add(updatedUserAccount, true);
            }
        }

        /// <summary>
        /// Enquery the modified accounts of the excel file defined with the path variable and convert it to the related user and group objects
        /// </summary>
        /// <returns></returns>
        private static bool LoadAccounts()
        {
            IQueryable<User> loginAccounts =
                from c in
                    ExcelQueryInitializer.User.WorksheetRange<User>(ConfigManager.GetSetting("FirstUsedCell"),
                                                                     ConfigManager.GetSetting("LastUsedCell"),
                                                                     ConfigManager.GetSetting("WorkSheetName"))
                select c;

            List<User> userToIrrevocableDelete = loginAccounts.ToList().FindAll(account => account.IsIrrevocableDeleted());
            ModifiedUsers = 
                loginAccounts.ToList().FindAll(account => account.IsModified());

            if (ModifiedUsers.Count == 0 && userToIrrevocableDelete.Count == 0) return false;
            ConfigManager.Log.DebugFormat("{0} geänderte Benutzer gefunden",ModifiedUsers.Count);

            var userToDelete = ModifiedUsers.ToList().FindAll(user => user.IsDeleted());
            userToDelete = userToDelete.Concat(loginAccounts.ToList().FindAll(user => user.IsIrrevocableDeleted() && user.AccountID != 0)).ToList();
            ConfigManager.Log.DebugFormat("{0} zu löschende Benutzer gefunden", userToDelete.Count);

            userToDelete.ForEach(CreateDeletedUserAccountLoginAccountMap);
            ModifiedUsers = ModifiedUsers.Except(userToDelete).ToList();
            ModifiedUsers.ForEach(CreateLoginAccountPasswordMap);
            ModifiedUsers.ForEach(CreateUserAccountLoginAccountMap);

            if (ModifiedUsers.Count > 0) ConfigManager.Log.Info("Geänderte Benutzer wurden erkannt");
            ConfigManager.Log.Info("Benutzer werden aktualisiert/erstellt");
            return true;
        }

        /// <summary>
        /// Method checks wheter a user with a given id exists on the server
        /// </summary>
        /// <param name="id">The id of a user</param>
        /// <param name="user">User instance to prove existence</param>
        /// <returns>True, if the user exists</returns>
        private static bool UserExistsOnServer(int id=0, User user = null)
        {
            try
            {
                if (id == 0 && user != null)
                {
                    id = user.AccountID;
                }

                AcademiesVault.UserOperations.GetUserAccount(id);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Method checks wheter a given LoginAccount exists on the server
        /// </summary>
        /// <param name="loginAccount">The login account to prove existence on server</param>
        /// <returns>True, if the loginAccount exists</returns>
        private static bool LoginAccountExistsOnServer(LoginAccount loginAccount)
        {
            return MFilesServerApplication.LoginAccountOperations.GetLoginAccounts().Cast<LoginAccount>().Any(account => account.UserName == loginAccount.UserName);
        }

        /// <summary>
        /// Helper Method for retrieving the id of a given user
        /// </summary>
        /// <param name="user">User to look for the id</param>
        /// <returns>The id of a given user or 0 in case of a not existing user</returns>
        private static int GetUserID(UserAccount user)
        {
            return (from UserAccount userAccount in AcademiesVault.UserOperations.GetUserAccounts() where userAccount.LoginName == user.LoginName select userAccount.ID).FirstOrDefault();
        }

        /// <summary>
        /// Helper method for converting a given user into a loginaccount and useraccount and adding them to a DeletedUserAccountLoginAccountMap
        /// </summary>
        /// <param name="user">The imported user</param>
        private static void CreateDeletedUserAccountLoginAccountMap(User user)
        {
            var loginAccount = (LoginAccountClass) user;
            var userAccount = (UserAccountClass) user;
            DeletedUserAccountLoginAccountMap.Add(userAccount, loginAccount);
        }

        /// <summary>
        /// Helper method for converting a given user into a loginaccount and useraccount and adding them to the userAccountLoginAccountMap
        /// </summary>
        /// <param name="user">The imported user</param>
        private static void CreateUserAccountLoginAccountMap(User user)
        {
            var loginAccount = (LoginAccountClass) user;
            var userAccount = (UserAccountClass) user;

            UserAccountLoginAccountMap.Add(userAccount, loginAccount);
        }

        /// <summary>
        /// Helper method for converting a given user into a loginaccount and adding it to the AccountPasswordMap
        /// </summary>
        /// <param name="user">The imported user</param>
        private static void CreateLoginAccountPasswordMap(User user)
        {
            var account = (LoginAccountClass) user;
            account.Enabled = user.Enabled;
            AccountPasswordMap.Add(account, user.MFilesPassword);
        }
    }
}